package com.nali.dal.mongo.explain.query;

import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Pattern;

import com.mongodb.BasicDBObject;
import com.mongodb.DBCollection;
import com.nali.dal.exception.SyntaxException;
import com.nali.dal.expression.ExpressionValue;
import com.nali.dal.expression.PropertyExpression;
import com.nali.dal.expression.query.Criteria;
import com.nali.dal.expression.query.CriteriaType;
import com.nali.dal.mongo.util.DalCollectionUtils;

/**
 * Find<br>&nbsp;
 * 收集整理Criteria以便生成PreparedFind的类
 * @author sam Created on 2011-3-8
 */
public class Find {
	private static Pattern commaSplitor=Pattern.compile("\\s*,\\s*");
	private List<Group> groupFunctions;
	private Condition condition=new Condition();
	private Set<String> groupBys;
	private LinkedHashMap<String, Boolean> ascs;
	private FindType type=FindType.normal;
	private ResultProperties properties=new ResultProperties();
	
	public Find add(PropertyExpression<Criteria> pc) {
		addPropertyCriteria(pc);
		return this;
	}
	
	public Find add(ExpressionValue<Criteria> expressionValue) {
		addCriteriaValue(expressionValue);
		return this;
	}
	
	public void addPropertyCriteria(PropertyExpression<Criteria> pc) {
		CriteriaType type = pc.getExpression().type;
		switch (type) {
		case groupBy:
			addGroupBy(pc);
			break;
		case groupFunction:
			addGroupFunction(pc.getExpression(), pc.getPropertyName(), null);
			break;
		case multiGroupFunction:
			addGroupFunction(pc.getExpression(), commaSplitor.split(pc.getPropertyName()), null);
			break;
		case orderBy:
			addOrderBy(pc);
			break;
		case select:
			properties.addSelect(pc);
			break;
		case where:
			addWhere(pc);
			break;
		case nativeExpression:
			addNativeExpression(pc);
			break;
		default:
			throw new SyntaxException("not supported type:" + type);
		}
	}

	private void addNativeExpression(PropertyExpression<Criteria> pc) {
		switch (pc.getExpression()) {
		case where:
			condition.addNativeWhere(pc.getPropertyName());
			break;
		default:
			throw new SyntaxException("not supported criteria:" + pc.getExpression());
		}
	}

	public void addCriteriaValue(ExpressionValue<Criteria> expressionValue) {
		PropertyExpression<Criteria> pc = expressionValue.getPropertyExpression();
		switch (pc.getExpression().type) {
		case select:
			properties.addSpecialSelect(pc.getExpression(), pc.getPropertyName(), expressionValue.getValue());
		case groupFunction:
			addGroupFunction(pc.getExpression(), pc.getPropertyName(), (String) expressionValue.getValue());
			break;
		case where:
			condition.add(pc.getExpression(), pc.getPropertyName(), expressionValue.getValue());
			break;
		default:
			throw new SyntaxException("not supported type:" + type);
		}
	}

	public PreparedFind prepare() {
		condition.check();
		if(type==FindType.groupBy) {
			properties.checkGroupBy(groupBys);
			BasicDBObject groupByBson = getGroups();
			BasicDBObject initialize=new BasicDBObject();
			StringBuilder reduce=new StringBuilder();
			Group.appendReduceHead(reduce);
			if(groupFunctions!=null) {
				for (Group group : groupFunctions) {
					if(group.getInitial()!=null) {
						initialize.putAll(group.getInitial());
					}
					reduce.append(group.getReduce()).append(";");
					//TODO 新版本加上finalize
				}
			}
			Group.appendFunctionTail(reduce);
			return new GroupByFind(groupByBson, condition, initialize, reduce.toString());
		} else {
			if(type==FindType.orderBy) {
				BasicDBObject orders = getOrders();
				return new OrderByFind(condition, properties, orders);
			} else {
				return new NormalFind(condition, properties);
			}
		}
	}

	private BasicDBObject getGroups() {
		BasicDBObject groups = new BasicDBObject();//TODO 可以优化一下各个size
		if(groupBys!=null) {
			for (String prop : groupBys) {
				groups.append(prop, true);
			}
		}
		return groups;
	}

	private BasicDBObject getOrders() {
		BasicDBObject orders;
		if(ascs!=null) {
			orders = new BasicDBObject(DalCollectionUtils.getMapSize(ascs.size()));
			for (Entry<String, Boolean> entry : ascs.entrySet()) {
				String prop = entry.getKey();
				Integer order;
				if(entry.getValue()==true) {
					order = 1;
				} else {
					order = -1;
				}
				orders.put(prop, order);
			}
		} else {
			orders = new BasicDBObject(1);
		}
		return orders;
	}

	private void addGroupFunction(Criteria criteria, String propertyName, String alias) {
		type = type.whenGroupFunction();
		if(groupFunctions==null) {
			groupFunctions=new LinkedList<Group>();
		}
		if(alias==null) {
			groupFunctions.add(Group.getGroup(criteria, propertyName));
		} else {
			groupFunctions.add(Group.getGroup(criteria, propertyName, alias));
		}
	}
	
	private void addGroupFunction(Criteria criteria, String[] props, String[] aliases) {
		type = type.whenGroupFunction();
		if(groupFunctions==null) {
			groupFunctions=new LinkedList<Group>();
		}
		if(aliases==null) {
			groupFunctions.add(Group.getGroup(criteria, props));
		} else {
			groupFunctions.add(Group.getGroup(criteria, props, aliases));
		}
	}

	private void addWhere(PropertyExpression<Criteria> cv) {
		condition.add(cv.getExpression(), cv.getPropertyName());
	}

	private void addOrderBy(PropertyExpression<Criteria> cv) {
		type = type.whenOrderBy();
		Boolean asc;
		if(cv.getExpression()==Criteria.asc) {
			asc=true;
		} else {
			asc=false;
		}
		if (ascs==null) {
			ascs=new LinkedHashMap<String, Boolean>();
		}
		Boolean lastValue = ascs.put(cv.getPropertyName(), asc);
		if(lastValue!=null) {
			throw new SyntaxException("orderBy's key appears twice:" + cv.getPropertyName());
		}
	}

	private void addGroupBy(PropertyExpression<Criteria> cv) {
		type = type.whenGroupBy();
		if (groupBys==null) {
			groupBys=new HashSet<String>();
		}
		groupBys.add(cv.getPropertyName());
	}
	public List<Map<String, Object>> find(List<Object> params, DBCollection collection, int start, int limit) {
		return prepare().find(params, collection, start, limit);
	}

	public long count(List<Object> params, DBCollection collection) {
		return prepare().count(params, collection);
	}

	public static PreparedFind parse(List<PropertyExpression<Criteria>> propertyExpressions, List<ExpressionValue<Criteria>> expressionValues) {
		Find find = new Find();
		for (ExpressionValue<Criteria> expressionValue : expressionValues) {
			find.addCriteriaValue(expressionValue);
		}
		for (PropertyExpression<Criteria> propertyExpression : propertyExpressions) {
			find.addPropertyCriteria(propertyExpression);
		}
		return find.prepare();
	}
}

enum FindType {
	normal(true, true),
	groupBy(true, false),
	orderBy(false, true),
	;
	private boolean canGroupBy=true;
	private boolean canOrderBy=true;
	private FindType(boolean canGroupBy, boolean canOrderBy) {
		this.canGroupBy = canGroupBy;
		this.canOrderBy = canOrderBy;
	}
	public FindType whenGroupFunction() {
		return whenGroupBy();
	}
	public FindType whenGroupBy() {
		if(canGroupBy) {
			return groupBy;
		} else {
			throw new SyntaxException("not supported both group by and order by");
		}
	}
	public FindType whenOrderBy() {
		if(canOrderBy) {
			return orderBy;
		} else {
			throw new SyntaxException("not supported both group by and order by");
		}
	}
}